/**
 * Title: Hyperbolic engine
 * Description: jFamilyTree Engine
 * Version: provider
 * Copyright: (c) 2001-2008
 * Terms of use:see license agreement at http://treebolic.sourceforge.net/en/license.htm
 * Author: Bernard Bou
 * Company: bsys
 * Update: Mon Mar 10 00:00:00 CEST 2008
 */
package jFamilyTree.control;

import java.awt.Point;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;

import javax.swing.JPopupMenu;

import jFamilyTree.Widget;
import jFamilyTree.component.PopupMenu;
import jFamilyTree.core.AbstractLayerOut;
import jFamilyTree.core.Complex;
import jFamilyTree.model.INode;
import jFamilyTree.model.Model;
import jFamilyTree.model.MountPoint;
import jFamilyTree.view.View;

/**
 * Controller
 * 
 * @author Bernard Bou
 */
public class Controller extends Commander
{
	// connected

	/**
	 * Connected widget
	 */
	private Widget theWidget;

	/**
	 * Connected model
	 */
	private Model theModel;

	/**
	 * Connected view
	 */
	private View theView;

	/**
	 * Connected layout agent
	 */
	private AbstractLayerOut theLayerOut;

	// behaviour

	/**
	 * Verbose status flag (use for debug purposes)
	 */
	static private final boolean verbose = false;

	// action

	/**
	 * Event types
	 */
	static public enum Event
	{
		SELECT, HOVER, DRAG, LEAVEDRAG, MOVE, ROTATE, FOCUS, MOUNT, LINK, POPUP
	}

	/**
	 * Action types
	 */
	public static enum Action
	{
		GOTO, SEARCH, FOCUS, LINK, MOUNT
	};

	/**
	 * Match scope
	 */
	public static enum MatchScope
	{
		LABEL, CONTENT, LINK, ID
	};

	/**
	 * Match mode
	 */
	public static enum MatchMode
	{
		EQUALS, STARTSWITH, INCLUDES
	};

	/**
	 * Match scopes
	 */
	static private final String[] theMatchScopeString = { "label", "content", "link", "id" };

	/**
	 * Match modes
	 */
	static private final String[] theMatchModeString = { "equals", "starts with", "includes" };

	// C O N T R O L L E R

	/**
	 * Constructor
	 */
	public Controller()
	{
		super();
		theWidget = null;
		theModel = null;
		theView = null;
		theLayerOut = null;
	}

	// C O N N E C T

	/**
	 * Connect with widget
	 * 
	 * @param thisWidget
	 *        widget
	 */
	public void connect(Widget thisWidget)
	{
		theWidget = thisWidget;
	}

	/**
	 * Connect
	 * 
	 * @param thisModel
	 *        model
	 */
	public void connect(Model thisModel)
	{
		theModel = thisModel;
	}

	/**
	 * Connect with view
	 * 
	 * @param thisView
	 *        view
	 */
	public void connect(View thisView)
	{
		theView = thisView;
	}

	/**
	 * Connect with layout agent
	 * 
	 * @param thisLayerOut
	 *        layerout
	 */
	public void connect(AbstractLayerOut thisLayerOut)
	{
		theLayerOut = thisLayerOut;
	}

	// R E F E R E N C E . F O R . C O M M A N D E R

	/*
	 * (non-Javadoc)
	 * 
	 * @see jFamilyTree.control.Commander#getModel()
	 */
	@Override
	protected Model getModel()
	{
		return theModel;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jFamilyTree.control.Commander#getView()
	 */
	@Override
	protected View getView()
	{
		return theView;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see jFamilyTree.control.Commander#getLayerOut()
	 */
	@Override
	protected AbstractLayerOut getLayerOut()
	{
		return theLayerOut;
	}

	// E V E N T S

	/**
	 * Handle events
	 * 
	 * @param thisEventType
	 *        event type
	 * @param thisParameter
	 *        event-specific object
	 */
	@SuppressWarnings("unchecked")
	public void handle(Event thisEventType, Object thisParameter)
	{
		switch (thisEventType)
		{
		case MOVE:
		{
			// translate as per vector(start,end)
			Pair<Complex> thisVector = (Pair<Complex>) thisParameter;
			theView.composeTranslate(thisVector.theFirst, thisVector.theSecond);
			break;
		}

		case ROTATE:
		{
			// rotate as per vector(start,end)
			Pair<Complex> thisVector = (Pair<Complex>) thisParameter;
			theView.composeRotate(thisVector.theFirst, thisVector.theSecond);
			break;
		}

		case HOVER:
		{
			INode thisNode = (INode) thisParameter;
			String thisLink = thisNode.getLink();

			// tooltip
			theView.setToolTip(thisNode);

			// cursor
			theView.setHoverCursor(thisLink != null && !thisLink.isEmpty());

			// status
			putNode(thisNode);
			break;
		}

		case DRAG:
		{
			theView.enterDrag();
			break;
		}

		case LEAVEDRAG:
		{
			theView.leaveDrag();
			break;
		}

		case SELECT:
		{
			break;
		}

		case FOCUS:
		{
			INode thisNode = (INode) thisParameter;
			theView.animateToCenter(thisNode, false);
			break;
		}

		case MOUNT:
		{
			INode thisNode = (INode) thisParameter;
			MountPoint thisMountPoint = thisNode.getMountPoint();
			if (thisMountPoint != null)
			{
				// mount/umount
				if (thisMountPoint instanceof MountPoint.Mounted)
					theWidget.umount(thisNode);
				else
				{
					MountPoint.Mounting thisMointingPoint = (MountPoint.Mounting) thisMountPoint;
					theWidget.mount(thisNode, thisMointingPoint.theURL);
				}
			}
			break;
		}

		case LINK:
		{
			INode thisNode = (INode) thisParameter;
			String thisLink = thisNode.getLink();
			String thisTarget = thisNode.getTarget();
			if (thisLink != null)
				linkTo(thisLink, thisTarget);
			break;
		}

		case POPUP:
		{
			Pair<Object> thisPair = (Pair<Object>) thisParameter;
			Point thisPoint = (Point) thisPair.theFirst;
			INode thisNode = (INode) thisPair.theSecond;
			popup(thisPoint.x, thisPoint.y, thisNode);
			break;
		}

		default:
			System.err.println("Unhandled event: " + thisEventType.toString());
		}
	}

	// D I S P A T C H

	/**
	 * Dispatch action
	 * 
	 * @param thisAction
	 *        action
	 * @param thisLink
	 *        url
	 * @param thisLinkTarget
	 *        url link target
	 * @param thisMatchTarget
	 *        match target
	 * @param thisMatchScope
	 *        match scope
	 * @param thisMatchMode
	 *        match mode
	 * @param thisNode
	 *        node
	 */
	public void dispatch(Action thisAction, String thisLink, String thisLinkTarget, String thisMatchTarget, MatchScope thisMatchScope, MatchMode thisMatchMode, INode thisNode)
	{
		switch (thisAction)
		{
		case GOTO:
			String thisString = theWidget.getTarget();
			String thisExpandedUrl = null;
			if (thisLink != null)
			{
				thisLink = decode(thisLink);
				thisExpandedUrl = PopupMenu.expandPercent(thisLink, thisString, thisNode);
			}
			linkTo(thisExpandedUrl, thisLinkTarget);
			break;

		case LINK:
			handle(Controller.Event.LINK, thisNode);
			break;

		case MOUNT:
			handle(Controller.Event.MOUNT, thisNode);
			break;

		case SEARCH:

			// target
			String thisEdit = theWidget.getTarget();
			String thisTarget;
			if (thisMatchTarget == null || thisMatchTarget.isEmpty())
				thisTarget = thisEdit;
			else
				thisTarget = PopupMenu.expandPercent(thisMatchTarget, thisEdit, thisNode);

			// search
			if (thisTarget != null)
			{
				String thisMessage = "search for node whose " + theMatchScopeString[thisMatchScope.ordinal()] + " " + theMatchModeString[thisMatchMode.ordinal()] + " <" + thisTarget + "> from " + thisNode.getLabel() + "\n";
				theWidget.putInformation(null, "searching " + thisMessage, false);
				INode thisResult = match(thisTarget, thisMatchScope, thisMatchMode, thisNode);
				if (thisResult != null)
				{
					theWidget.putInformation(thisTarget, thisMessage + thisResult, false);
					theView.animateToCenter(thisResult, false);
				}
				else
					theWidget.putInformation(thisTarget, thisMessage + "null", false);
			}
			break;

		case FOCUS:
			theView.animateToCenter(thisNode, false);
			break;

		default:
			System.err.println("Unsupported dispatch action=" + thisAction + " link=" + thisLink + " context=" + thisNode);
		}
	}

	// D I S P L A Y

	/**
	 * Display node
	 * 
	 * @param thisNode
	 *        node
	 */
	private void putNode(INode thisNode)
	{
		// data
		String thisLabel = thisNode.getLabel();
		String thisContent = thisNode.getContent();
		if (thisContent == null)
			thisContent = "";
		String thisLink = thisNode.getLink();
		if (thisLink != null && !thisLink.isEmpty())
			thisContent += " &lt;" + decode(thisLink) + "&gt;";
		MountPoint thisMountPoint = thisNode.getMountPoint();
		if (thisMountPoint != null && thisMountPoint instanceof MountPoint.Mounting)
		{
			MountPoint.Mounting thisMountingPoint = (MountPoint.Mounting) thisMountPoint;
			thisContent += " [mountpoint=" + thisMountingPoint.theURL + "]";
		}
		if (verbose)
			thisContent += " weight=" + thisNode.getWeight();
		theWidget.putInformation(thisLabel, thisContent, false);
	}

	// P O P U P

	public void popup(int x, int y, INode thisNode)
	{
		JPopupMenu thisMenu = PopupMenu.makePopup(this, theWidget.getTarget(), thisNode, theModel.theSettings);
		thisMenu.show(theView, x, y);
	}

	// M A T C H . N O D E

	/**
	 * Match node against for string
	 * 
	 * @param thisString
	 *        string to match
	 * @param thisScope
	 *        scope
	 * @param thisMode
	 *        mode
	 * @param thisNode
	 *        node
	 * @return node
	 */
	private INode match(String thisString, MatchScope thisScope, MatchMode thisMode, INode thisNode)
	{
		// match
		String thisTarget = null;
		switch (thisScope)
		{
		case CONTENT:
			thisTarget = thisNode.getContent();
			break;

		case LINK:
			thisTarget = thisNode.getLink();
			break;

		case ID:
			thisTarget = thisNode.getId();
			break;

		case LABEL:
		default:
			thisTarget = thisNode.getLabel();
			break;
		}

		// try to match this node
		if (thisTarget != null)
		{
			switch (thisMode)
			{
			case EQUALS:
				if (thisTarget.equals(thisString))
					return thisNode;
				break;

			case INCLUDES:
				if (thisTarget.indexOf(thisString) != -1)
					return thisNode;
				break;

			case STARTSWITH:
			default:
				if (thisTarget.startsWith(thisString))
					return thisNode;
				break;
			}
		}

		// try to match match this node's children
		for (INode thisChild : thisNode.getChildren())
		{
			INode thisResult = match(thisString, thisScope, thisMode, thisChild);
			if (thisResult != null)
				return thisResult;
		}
		return null;
	}

	/**
	 * Match node against string (wrapper)
	 * 
	 * @param thisString
	 *        string to match
	 * @param thisScope
	 *        scope (LABEL, CONTENT, LINK, ID)
	 * @param thisMode
	 *        mode (EQUALS, STARTSWITH, INCLUDES)
	 * @return node id
	 */
	public String match(String thisString, MatchScope thisScope, MatchMode thisMode)
	{
		INode thisNode = match(thisString, thisScope, thisMode, theModel.theTree.getRoot());
		if (thisNode != null)
			return thisNode.getId();
		return null;
	}

	// S E A R C H . F O R . N O D E . F R O M . L O C A T I O N

	/**
	 * Find node
	 * 
	 * @param vx
	 *        at view x position
	 * @param vy
	 *        at view y position
	 * @return found node or null
	 */
	public INode findNode(int vx, int vy)
	{
		Complex thisEuclidianLocation = theView.viewToUnitCircle(vx, vy);
		return theModel.theTree.findNodeAt(theModel.theTree.getRoot(), thisEuclidianLocation);
	}

	// F O C U S

	/**
	 * Focus node
	 * 
	 * @param thisNodeId
	 *        node id to get focus
	 */
	public void focus(String thisNodeId)
	{
		INode thisNode = theModel.theTree.findNodeById(theModel.theTree.getRoot(), thisNodeId);
		if (thisNode != null)
			theView.animateToCenter(thisNode, false);
	}

	// N A V I G A T I O N

	/**
	 * Decode encoded URL (for display)
	 * 
	 * @param thisString
	 *        encode URL string
	 * @return decoded URL string
	 */
	private String decode(String thisString)
	{
		try
		{
			return URLDecoder.decode(thisString, "UTF8");
		}
		catch (UnsupportedEncodingException e)
		{
			System.err.println("Can't decode " + thisString + " - " + e);
		}
		return thisString;
	}

	/**
	 * Follow hypertext link
	 * 
	 * @param thisHref
	 *        url string
	 * @param thisTarget
	 *        target frame
	 */
	public void linkTo(String thisHref, String thisTarget)
	{
		if (thisHref == null)
			return;

		// reference hook : find node with identifier
		if (thisHref.startsWith("#"))
		{
			String thisBookmark = thisHref.substring(1);
			INode thisFocus = theModel.theTree.findNodeById(theModel.theTree.getRoot(), thisBookmark);
			if (thisFocus != null)
			{
				theView.animateToCenter(thisFocus, false);
				return;
			}
		}

		String thisDecodedLink = decode(thisHref);

		// status
		theWidget.putInformation("LINK", "Link to " + thisDecodedLink + " ," + thisTarget, true);
		theWidget.getContext().showStatus("Link to " + thisDecodedLink);

		// jump link
		if (!theWidget.getContext().linkTo(thisDecodedLink, thisTarget))
			theWidget.reinit(thisDecodedLink);
	}

	// S P A C E . C O N V E R S I O N

	/**
	 * Convert view space to unit circle
	 * 
	 * @param thisPoint
	 *        view space coordinate
	 * @return unit circle coordinate
	 */
	public Complex viewToUnitCircle(Point thisPoint)
	{
		return theView.viewToUnitCircle(thisPoint.x, thisPoint.y);
	}
}
